package org.bultreebank.searching;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.IndexReader;
import org.bultreebank.document_repository.interfaces.IndexesDocumentsManager;
import org.bultreebank.index.schema.management.interfaces.IndexContextsManager;
import org.bultreebank.index.schema.management.interfaces.IndexingSchemaManager;
import org.bultreebank.index.utils.Constants;
import org.bultreebank.index.utils.Factory;
import org.bultreebank.index.utils.Pair;
import org.bultreebank.index.utils.SearchUtils;
import org.bultreebank.index.utils.Utils;
import org.bultreebank.operators.Intersection;
import org.bultreebank.operators.Operator;
import org.bultreebank.operators.Union;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import clark.xpath.MainXPath;


public class SearchImpl implements SearchManager, Constants{

	private IndexingSchemaManager indexManager;
	private IndexContextsManager contextsManager;
	private IndexesDocumentsManager documentsManager;
	private String searchEnginePath;
	private Stack<Object> queryStack = new Stack<Object>();
	private Map<String, String> searchAnalyzersMap;
	private String currentLang;
	private Analyzer analyzer;
	

	public SearchImpl(Map<String, String> searchAnalyzersMap, IndexingSchemaManager indexManager,
			IndexContextsManager contextsManager, IndexesDocumentsManager indexDocumentsManager)
	throws Exception{
		this.searchAnalyzersMap = searchAnalyzersMap;
		if (searchAnalyzersMap.size() == 1)
		{
			currentLang = searchAnalyzersMap.keySet().iterator().next();
			String className = searchAnalyzersMap.get(currentLang);
			analyzer = (Analyzer)Factory.createInstance(className);
		}
		this.indexManager = indexManager;
		this.contextsManager = contextsManager;
		this.documentsManager = indexDocumentsManager; 
		this.searchEnginePath = indexManager.getIndexRepositoryPath();
	}
	
	public Map<String, List<String>> getFieldContentDocument(String index,
			String fieldName, String context) throws Exception
	{
		
		Document indexSchema = indexManager.getIndexSchema(index);
		contextsManager.setIndexSchemaDocument(indexSchema);
		String indexFullPath = getIndexFullPath(index, context);
		Map<String, List<String>> result = new HashMap<String, List<String>>();
		Map<String, List<String>> contextsFields = contextsManager.getContextRetrievableFieldsRealIDs();
		
		String hierarchyContext = getHierarchyContext(context, indexSchema);
		if (hierarchyContext != null)
		{
			List<String> fields = contextsFields.get(hierarchyContext);
			if (!fields.contains(fieldName))
				throw new Exception("The specified field for extraction doesn't exist!");
			
			IndexReader reader = SearchUtils.openReader(indexFullPath);
			int numDocs = reader.numDocs();
			 
			for (int i = 0; i < numDocs; i++)
			{
				if (!IndexReader.isLocked(indexFullPath) 
						&& !reader.isDeleted(i))
				{
					org.apache.lucene.document.Document doc = reader.document(i);
					String docID = doc.get(DOC_ID);
					StringTokenizer st = new StringTokenizer(doc.get(fieldName));
					List<String> tokens = new ArrayList<String>();
					while (st.hasMoreTokens())
					{
						String token = st.nextToken();
						tokens.add(token);
					}
					result.put(docID, tokens);
				}
			}
			
			reader.close();
		}
		return result;
	}
	
	
	private void generateQueryStack(String indexDIR,
			String context, List<Node> fieldNodes)throws Exception
			{
			if (context == null)
				context = GLOBAL_CONTEXT;
			Map<String, String> fieldTypes = contextsManager.getContextFieldIDs(context);
			for (int i = 0; i < fieldNodes.size(); i++)
			{
				Element fieldElem = (Element) fieldNodes.get(i);
				String fieldName = fieldElem.getNodeName(); 
				if (fieldName.equals(FIELD))
				{
					Set<String> fieldResult = null;
					String searchBy = fieldElem.getAttribute("searchBy");
					if (!fieldTypes.containsKey(searchBy))
						fieldResult = new HashSet<String>();
					else
					{
						String type = fieldTypes.get(searchBy);
						boolean isKeyword = false;
						if (type.equals(KEYWORD))
							isKeyword = true;
							fieldResult = fieldSearch(fieldElem, indexDIR, isKeyword);
					}
					queryStack.push(fieldResult);
				}
				else
				{
					Operator<Set<String>> op = null;
					if (fieldElem.getNodeName().equals(AND_TAG))
						op	 = new Intersection(new ArrayList<Set<String>>());
					if (fieldElem.getNodeName().equals(OR_TAG))
						op = new Union(new ArrayList<Set<String>>());
					queryStack.push(op);
					List<Node> nodes = MainXPath.processXPath("*", fieldElem);
					generateQueryStack(indexDIR, context, nodes);
				}
			}
	}
	private Set<String> processQueryStack()
	{
		Set<String> result = null;
		List<Set<String>> fieldSearchResults = new ArrayList<Set<String>>();
		while (!queryStack.isEmpty())
		{
			
			Object top = queryStack.pop();
			if (top instanceof Intersection || top instanceof Union)
			{
				if (!fieldSearchResults.isEmpty())
				{
					Operator op = ((Operator)top);
					op.setArgumentList(fieldSearchResults);
					Set<String> resultSet = (Set<String>)op.evaluateOperator();
					fieldSearchResults.clear();
					fieldSearchResults.add(resultSet);
				}
			}
			else
				fieldSearchResults.add((Set<String>) top);
		}
		if (fieldSearchResults.size() > 0)
		{
			Intersection op = new Intersection(fieldSearchResults);
			result = op.evaluateOperator();
		}
		
		return result;
	}
	
	
	private Set<String> searchContext(Element root,Element parent,
			String indexID, String contextID) throws Exception
	{
		String rootStr = root.getNodeName();
		List<Node> fieldNodes = MainXPath.processXPath("*[name()!=\"context\"]", root);
		List<Node> contextChildren = MainXPath.processXPath(CONTEXT, root);	
		boolean complexField = false, isRootContext = false, hasOnlyContextChildren = false;
		Node parentN = root.getParentNode();
		if (!fieldNodes.isEmpty() && !contextChildren.isEmpty())
			complexField = true;
		if (fieldNodes.isEmpty() && !contextChildren.isEmpty())
			hasOnlyContextChildren = true;
		if (!parentN.getNodeName().equals(CONTEXT))
			isRootContext = true;
		List<Set<String>> arguments = new ArrayList<Set<String>>();
		Intersection parentIDsIntersection = new Intersection(arguments);
		if (!contextChildren.isEmpty())
		{
			for (int i = 0; i < contextChildren.size(); i++)
			{
				Node contextChild = contextChildren.get(i);
				Element contextElem = (Element) contextChild;
				String contextStr = contextElem.getAttribute(VALUE);
				Pair<String, String> searchableContexts = contextsManager.getContextIDs(contextStr);
				String contextDependency = searchableContexts.getSecond();
				Set<String> result = searchContext(contextElem, root, indexID, contextDependency);
				if (result.isEmpty())
					return new HashSet<String>();
				if (hasOnlyContextChildren || complexField || isRootContext)
					arguments.add(SearchUtils.getParentDocumentIDs(result));
				else
					arguments.add(result);
				if (complexField)
				{
					String context = root.getAttribute(VALUE);
					Pair<String, String> pair = contextsManager.getContextIDs(context);
					if (pair != null)
					{
						String contextDir = pair.getFirst();
						String indexDir = indexPath(indexManager.getInternalIndexID(indexID), contextDir);
						if (rootStr.equals(FIELD))
						queryStack.push(new Intersection(new ArrayList<Set<String>>()));
						generateQueryStack(indexDir, pair.getSecond(), fieldNodes);
						Set<String> resultSet = processQueryStack();
						if (isRootContext && hasOnlyContextChildren)
							arguments.add(SearchUtils.getParentDocumentIDs(result));
						else
							arguments.add(resultSet);
					}
				}
						
			}
			return parentIDsIntersection.evaluateOperator();
		}
		else
		{
				String contextStr = root.getAttribute(VALUE);
				Pair<String, String> searchableContexts = contextsManager.getContextIDs(contextStr);
				String contextDir = searchableContexts.getFirst();
				String contextDependency = searchableContexts.getSecond();
				String indexDir = indexPath(indexManager.getInternalIndexID(indexID), contextDir);
				fieldNodes = MainXPath.processXPath("*", root);
				generateQueryStack( indexDir, contextDependency, fieldNodes);
				Set<String> result = processQueryStack();
				return result;
		}
	}
	
	
	public Map<String, Set<String>> search(Document queryDoc, String indexID)
	throws Exception
	{
		contextsManager.setIndexSchemaDocument(indexManager.getIndexSchema(indexID));
		Element queryRoot = queryDoc.getDocumentElement();
		String lang = queryRoot.getAttribute(LANG); 
		if (lang == null || lang.equals(EMPTY_STR))
			throw new Exception("The language of documents to be retrieved in is not specified in the query");
		adjustAnalyzer(lang);
		List<Node> contexts = MainXPath.processXPath("context", queryRoot);
		Map<String, Set<String>> result = new HashMap<String, Set<String>>();
		if (contexts.size() > 0)
		{
			for (Node context: contexts)
			{
				List<Node> contextsT = MainXPath.processXPath("context", context);
				if (!contextsT.isEmpty())
				{
					Set<String> contextSearchResult = searchContext((Element)context, null, indexID, null);
					Set<String> cutIDs = new HashSet<String>();
					Iterator<String> contextResIter = contextSearchResult.iterator();
					while (contextResIter.hasNext())
					{
						String docID = contextResIter.next();
						if (docID.indexOf(SPEC_CHAR) > -1)
						{
							int index = docID.lastIndexOf(SPEC_CHAR) + SPEC_CHAR.length();
							docID = docID.substring(index + 1);
						}
						cutIDs.add(docID);
					}
					if (!cutIDs.isEmpty())
						result.put(((Element) context).getAttribute(VALUE), cutIDs);
				}
				else
				{
					List<Node> nodes = MainXPath.processXPath("*[name()!=\"context\"]", context);
					if (nodes.size() > 0)
					{
						String contextP = ((Element)context).getAttribute(VALUE);
						Pair<String, String> pair = contextsManager.getContextIDs(contextP);
						if (pair == null)
							continue;
						String contextDir = pair.getFirst();
						String indexDir = indexPath(indexManager.getInternalIndexID(indexID), contextDir);
						generateQueryStack(indexDir, pair.getSecond(), nodes);
						Set<String> resultSet = processQueryStack();
						if (!resultSet.isEmpty())
							result.put(((Element) context).getAttribute(VALUE), resultSet);
					}
				}
			}
		}
		List<Node> nodes = MainXPath.processXPath("*[name()!=\"context\"]", queryRoot);
		if (nodes.size() > 0)
		{
			String indexDir = getIndexFullPath(indexID, null);
			generateQueryStack(indexDir, null, nodes);
			Set<String> resultSet = processQueryStack();
			if (!resultSet.isEmpty())
				result.put(GLOBAL_CONTEXT, resultSet);
		}
		return result;
	}
	
	private String indexPath(String indexID, String context)
	{
		return searchEnginePath + SEPARATOR + indexID + SEPARATOR + context;
	}
	
	private String getIndexFullPath(String indexID, String context) throws Exception
	{
		if (!indexManager.ifIndexExists(indexID))
			throw new Exception ("The index " + indexID + " doesn't exist!");
		String internalIndexID = indexManager.getInternalIndexID(indexID);
		if (context == null || context.equals(EMPTY_STR))
			return searchEnginePath + SEPARATOR + internalIndexID + SEPARATOR + GLOBAL_CONTEXT;
		String internalContextID = contextsManager.getRealContextID(context);
		if (internalContextID.equals(EMPTY_STR))
			throw new Exception ("The context specified doesn't exist!");
		return searchEnginePath + SEPARATOR + internalIndexID + SEPARATOR + internalContextID;
	}
	private String getHierarchyContext(String context, Document indexSchema) throws Exception
	{
		if (context == null || context.equals(EMPTY_STR))
			return null;
		Map<String, List<String>> contextsFields = contextsManager.getContextRetrievableFieldsRealIDs();
		Set<String> contexts = contextsFields.keySet();
		Iterator<String> iter = contexts.iterator();
		String hierarchyContext = null;
		int depth = 0;
		while (iter.hasNext())
		{
			String key = iter.next();
			StringTokenizer st = new StringTokenizer(key, COLUMN);
			int num = st.countTokens();
			if (num > depth && key.indexOf(context) > -1)
			{
				depth = num;
				hierarchyContext = key;
			}
		}
		if (hierarchyContext != null)
			return hierarchyContext;
		return EMPTY_STR;
		
	}
	private Set<String> fieldSearch (Element fieldElem,
			String indexDIR,
			boolean keyword) throws Exception
	{
		String searchBy = fieldElem.getAttribute(SEARCH_BY);
		if (searchBy == null || searchBy.equals(EMPTY_STR))
			throw new Exception(
					"One of the searchable fields is not specified!");
		String simpleQuery = null;
		NodeList ch = fieldElem.getChildNodes();
		if (ch != null && ch.getLength() > 0)
			simpleQuery = generateLuceneTermsOperators(fieldElem);
		Set<String> res = SearchUtils.luceneFieldsSearch(analyzer, indexDIR, simpleQuery,
		searchBy, true, keyword);
		
		return res;

	}
	private String generateLuceneTermsOperators(Element luceneQueryElement)
	throws Exception {
		StringBuffer query = new StringBuffer();
		NodeList children = luceneQueryElement.getChildNodes();
		String searchBy = luceneQueryElement.getAttribute(SEARCH_BY);
			
		Node luceneQuery = children.item(0);
		boolean ifRangeQuery = false;
		for (int i = 0; i < children.getLength(); i++) {
			luceneQuery = children.item(i);
			String queryRoot = luceneQuery.getNodeName();
			if (queryRoot.equals(ARGUMENT))
			{
				String literalV = ((Element) luceneQuery)
						.getAttribute(LITERAL_VAL);
				if (literalV.indexOf(WHITESPACE) > 0)
					literalV = "\"" + Utils.normalizeTerm(literalV) + "\"";
				else //if (literalValue.indexOf("*") > 0 || literalValue.indexOf("?") > 0)
					literalV = Utils.normalizeTerm(literalV);
				if (searchBy.equals(DOC_ID))
				{
					String internalValue = documentsManager.getInternalDocID(literalV);
					if (internalValue != null && !internalValue.equals(EMPTY_STR))
						literalV = internalValue;
				}
				String included = ((Element) luceneQuery)
						.getAttribute(INCLUDED_ATTR);
				String pOP = ((Element) luceneQuery)
						.getAttribute(POST_OPERATOR);
				if (included.equals(NO))
					query.append(WHITESPACE + NOT_SIGN + WHITESPACE);
				String op = EMPTY_STR;
				if (pOP.equals(SIMILAR_SPELLING))
					op = SIMILAR_SPELLING_SIGN;
				if (i == children.getLength() - 1)
					if (op.equals(SIMILAR_SPELLING_SIGN)
							|| op.equals(DISTANCE_SIGN))
						query.append(literalV + op + WHITESPACE);
				    else if (!op.equals(EMPTY_STR))
						query.append(literalV + WHITESPACE + op + WHITESPACE);
				    else query.append(literalV);
				else if (op.equals(SIMILAR_SPELLING_SIGN)
						|| op.equals(DISTANCE_SIGN))
					query.append(literalV + op + WHITESPACE + OR_SIGN
							+ WHITESPACE);
				else
					query.append(literalV + WHITESPACE + op + WHITESPACE
							+ OR_SIGN + WHITESPACE);
		
			} else if (queryRoot.equals(RANGE_TAG)) {
				ifRangeQuery = true;
				String minLiteralValue = ((Element) luceneQuery)
						.getAttribute(MIN_VAL);
				String maxLiteralValue = ((Element) luceneQuery)
						.getAttribute(MAX_VAL);
				minLiteralValue = Utils.normalizeTerm(Utils.normalizeFromDate(minLiteralValue));//.toLowerCase();
				maxLiteralValue = Utils.normalizeTerm(Utils.normalizeToDate(maxLiteralValue));//.toLowerCase();
				query.append(BROKEN_LEFT_BRACKET + WHITESPACE + minLiteralValue + WHITESPACE
						+ RANGE_SIGN + WHITESPACE + maxLiteralValue
						+ WHITESPACE + BROKEN_RIGHT_BRACKET);
			} else
				query.append(translateLuceneQuery((Element) luceneQuery));
		
		}
		if (!ifRangeQuery) {
			String queryStr = query.toString();
			if (queryStr.endsWith(OR_SIGN)) {
				int ind = queryStr.lastIndexOf(OR_SIGN);
				queryStr = queryStr.substring(0, ind);
			}
			if (queryStr.endsWith(AND_SIGN)) {
				int ind = queryStr.lastIndexOf(AND_SIGN);
				queryStr = queryStr.substring(0, ind);
			}
		}
		return query.toString();
	}

	private StringBuffer translateLuceneQuery(Element query) {
		String operatorType = query.getNodeName();
		String operator = null;
		if (operatorType.equals(AND_TAG))
			operator = AND_SIGN;
		else
			operator = OR_SIGN;
		StringBuffer luceneQuery = new StringBuffer();
		NodeList children = query.getChildNodes();

		for (int i = 0; i < children.getLength(); i++) {
			Node dd = children.item(i);
			if (dd.getNodeName().equals(ARGUMENT)) {
				String literalValue = ((Element) dd).getAttribute(LITERAL_VAL);
				if (literalValue.indexOf(WHITESPACE) > 0)
					literalValue = "\"" + Utils.normalizeTerm(literalValue) + "\"";
				else //if (literalValue.indexOf("*") > 0 || literalValue.indexOf("?") > 0)
					literalValue = Utils.normalizeTerm(literalValue);
					
				
				String included = ((Element) dd).getAttribute(INCLUDED_ATTR);
				String pOP = ((Element) dd).getAttribute(POST_OPERATOR);
				if (!literalValue.equals(EMPTY_STR)) {

					if (included.equals(NO))
						luceneQuery.append(WHITESPACE + NOT_SIGN + WHITESPACE);
					else
						luceneQuery.append(literalValue);
					if (pOP.equals(SIMILAR_SPELLING))
						luceneQuery.append(WHITESPACE + SIMILAR_SPELLING_SIGN
								+ WHITESPACE);
					if (i != children.getLength() - 1)
						luceneQuery.append(WHITESPACE + operator + WHITESPACE);
				}
			}

			if (dd.getNodeName().equals(AND_TAG)) {
				luceneQuery.append(WHITESPACE + LEFT_BRACKET);
				luceneQuery.append(translateLuceneQuery((Element) dd));
				luceneQuery.append(RIGHT_BRACKET + WHITESPACE);
				if (i != children.getLength() - 1) {
					if (dd.getParentNode().getNodeName().equals(AND_TAG))
						luceneQuery.append(WHITESPACE + AND_SIGN + WHITESPACE);
					if (dd.getParentNode().getNodeName().equals(OR_TAG))
						luceneQuery.append(WHITESPACE + OR_SIGN + WHITESPACE);
				}
			} else if (dd.getNodeName().equals(OR_TAG)) {
				luceneQuery.append(LEFT_BRACKET);
				luceneQuery.append(translateLuceneQuery((Element) dd));
				luceneQuery.append(RIGHT_BRACKET);
				if (i != children.getLength() - 1) {
					if (dd.getParentNode().getNodeName().equals(AND_TAG))
						luceneQuery.append(WHITESPACE + AND_SIGN + WHITESPACE);
					if (dd.getParentNode().getNodeName().equals(OR_TAG))
						luceneQuery.append(WHITESPACE + OR_SIGN + WHITESPACE);
				}
			}
		}

		return luceneQuery;
	}
	private void adjustAnalyzer(String lang)
	{
		if (!searchAnalyzersMap.containsKey(lang))
			analyzer = new StandardAnalyzer();
		else if (currentLang != null && !currentLang.equals(lang))
		{
			analyzer = (Analyzer)Factory.createInstance(searchAnalyzersMap.get(lang));
			if (analyzer == null)
				analyzer = new StandardAnalyzer();
		}
		else if (currentLang == null && searchAnalyzersMap.containsKey(lang))
			analyzer = (Analyzer)Factory.createInstance(searchAnalyzersMap.get(lang));
		currentLang = lang;
		
	}

}
